/*

 You may freely copy, distribute and reuse the code in this example.
 NeXT disclaims any warranty of any kind, expressed or implied,
 as to its fitness for any particular use.

 CompositeView implements a view with three horizontal, equal-sized areas.
 The left-most area is the "source," the middle area is the "destination,"
 and the right-most area is the "result." CompositeView assures that the
 contents of the result area is always generated by compositing the other
 two areas using the compositing mode set in the setOperator: method.
 It is also possible to change the contents, color, and alpha of the
 source and destination areas; see the methods setSourceColor:, 
 setSourceAlpha:, etc.

 CompositeView also demonstrates some of the drag & drop features of
 NeXTSTEP 3.0 by acting as a destination for colors & images.

 CompositeView written by Bruce Blumberg and Ali Ozer.

 Color support, NXColorPanel, NXColorWells, and NXImage added during 1990 by Ali.
 Color dragging support added Feb 9, 1992, by Ali.
 Image dragging support added May 27, 1992, by Ali.
 Image dragging problem fixed Sep 29, 1992 (for 3.1), by Ali. See QAP appkit.878.

*/

#import <appkit/appkit.h>
#import "CompositeView.h"


@implementation CompositeView

// The possible draw modes for the source.

#define TRIANGLE 0
#define CIRCLE   1
#define DIAMOND  2
#define HEART    3
#define FLOWER   4
#define CUSTOM   5

// initFrame: creates the view, initializes the rectangles that define the
// three areas described above, and creates the bitmaps used for rendering the
// source and destination bitmaps. newFrame: is a convenience method.

- initFrame:(const NXRect *)tF
{
    // Initialize the view
    [super initFrame:tF];

    // Make rectangles for source, destination and result
    sRect = bounds;
    sRect.size.width /= 3.0;
    dRect = sRect;
    dRect.origin.x = sRect.size.width;
    rRect = dRect;
    rRect.origin.x = dRect.origin.x + dRect.size.width;

    // Create source, destination, and result images.

    [(source = [[NXImage allocFromZone:[self zone]] initSize:&sRect.size])
	useDrawMethod:@selector(drawSource:) inObject:self];
    [source setBackgroundColor:NX_COLORCLEAR];
    
    [(destination = [[NXImage allocFromZone:[self zone]] initSize:&dRect.size])
	useDrawMethod:@selector(drawDestination:) inObject:self];
    [destination setBackgroundColor:NX_COLORCLEAR];

    [(result = [[NXImage allocFromZone:[self zone]] initSize:&dRect.size])
	useDrawMethod:@selector(drawResult:) inObject:self];
    [result setBackgroundColor:NX_COLORCLEAR];

    // Set the default operator and source picture. No need to set the default
    // colors; these are read from the .nib file when the outlets to the wells
    // are estanblished.

    operator = NX_COPY;
    sourcePicture = TRIANGLE;

    // Tell the application that alpha should be allowed in the color panel
    // and dragged colors. Most apps do not want to bother with this.

    [NXApp setImportAlpha:YES];

    // Finally, register for dragging colors and files. 

    [self registerForDraggedTypes:&NXColorPboardType count:1];
    [self registerForDraggedTypes:&NXFilenamePboardType count:1];

    return self;
}

// Get handles to the wells and read their initial colors.

- setSourceColorWell:anObject
{
    sourceColorWell = anObject;
    sourceColor = [anObject color];
    return self;
}

- setDestColorWell:anObject
{
    destColorWell = anObject;
    destColor = [anObject color];
    return self;
}

- setBackColorWell:anObject
{
    backColorWell = anObject;
    backgroundColor = [anObject color];
    return self;
}

// drawSource creates the source image in the source bitmap. Note that
// drawSource does not render in the view; it renders in the bitmap only.

- drawSource:image
{	
    NXPoint zeroPoint = {0.0, 0.0};

    NXSetColor(sourceColor);
    PSnewpath();
    switch (sourcePicture) {

	case TRIANGLE: 
 	    PSmoveto (0.0, 0.0);
	    PSlineto (0.0, sRect.size.height);
	    PSlineto (sRect.size.width, sRect.size.height);
	    break;

	case CIRCLE:
	    PSscale (sRect.size.width, sRect.size.height);
	    PSarc (0.5, 0.5, 0.4, 0.0, 360.0);  // diameter is 80% of area
 	    break;

	case DIAMOND:
 	    PSmoveto (0.0, sRect.size.height / 2.0);
	    PSlineto (sRect.size.width / 2.0, 0.0);
	    PSlineto (sRect.size.width, sRect.size.height / 2.0);
	    PSlineto (sRect.size.width / 2.0, sRect.size.height);
	    break;

	case HEART:
	    PSscale (sRect.size.width, sRect.size.height);
	    PSmoveto (0.5, 0.5);
	    PScurveto (0.3, 1.0, 0.0, 0.5, 0.5, 0.1);
	    PSmoveto (0.5, 0.5);			
	    PScurveto (0.7, 1.0, 1.0, 0.5, 0.5, 0.1);  
	    break;

	case FLOWER:
	    PSscale (sRect.size.width, sRect.size.height);
	    PStranslate (0.5, 0.5);
	    PSmoveto (0.0, 0.0); 
            {int cnt;
	     for (cnt = 0; cnt < 6; cnt++) {
		PSrotate (60.0);
		PScurveto (0.4, 0.5, -0.4, 0.5, 0.0, 0.0);
	     }
	    }
	    break;

	 case CUSTOM:
	    if (!customImage) {
		customImage = [[NXImage allocFromZone:[self zone]] 
				initSize:&rRect.size];
		[customImage setScalable:YES];
		[customImage useFromSection:"DefaultCustomImage.eps"];
	    }
	    [customImage composite:NX_SOVER toPoint:&zeroPoint];
	    break;
 
	default:
	    break;
    }
    PSclosepath();
    PSfill();

    return self;
}

// drawDestination creates the destination image in the destination bitmap. 
// Like drawSource, drawDestination only draws in the bitmap, not the view.

- drawDestination:image
{
    NXSetColor(destColor);
    PSnewpath();
    PSmoveto(dRect.size.width, 0.0);
    PSlineto(dRect.size.width, dRect.size.height);
    PSlineto(0.0, dRect.size.height);
    PSclosepath();
    PSfill();
    return self;
}

// drawResults creates the resulting image, formed by compositing the
// source image after the destination image with the specified operator. 

- drawResult:image
{
    NXPoint zeroPoint = {0.0, 0.0};

    [destination composite:NX_COPY toPoint:&zeroPoint];
    [source composite:operator toPoint:&zeroPoint];
    return self;
}
    
// setSourcePicture allows setting the picture to be drawn in the source
// bitmap. Buttons connected to this method should have tags that are 
// set to the various possible pictures (see the "#define"s, above).
//
// After setting the sourcePicture instance variable, setSourcePicture redraws
// the bitmap and updates the view to reflect the new configuration.

- setSourcePicture:sender
{
    sourcePicture = [sender selectedTag];
    [source recache];
    [result recache];
    [self display];
    return self;
}

- (BOOL)changeCustomImageTo:newImage
{
    if (newImage) {
	[newImage setSize:&rRect.size];
	[newImage setScalable:YES];
	if ([newImage lockFocus]) {	// Is this a good image indeed?
	    [newImage unlockFocus];
	    [customImage free];
	    customImage = newImage;
	    if (sourcePicture != CUSTOM) {
		sourcePicture = CUSTOM;
		[sourcePictureMatrix selectCellWithTag:CUSTOM];
	    }
	    [source recache];
	    [result recache];
	    [self display];
	    return YES;
	}
    }
    return NO;
}

- changeCustomImage:sender
{
    if ([[OpenPanel new] runModalForTypes:[NXImage imageFileTypes]]) {
	const char *fileName = [[OpenPanel new] filename];
	(void)[self changeCustomImageTo:[[NXImage allocFromZone:[self zone]] initFromFile:fileName]];
    }

    return self;
}

// The following methods change the colors and update
// the source or destination bitmaps and the view to reflect the change.
// They should typically be called by a control capable of returning
// a color (for instance, an NXColorWell).

- changeSourceColor:sender
{
    [self changeSourceColorTo:[sender color] andDisplay:YES];
    return self;
}

- changeDestColor:sender
{
    [self changeDestColorTo:[sender color] andDisplay:YES];
    return self;
}

- changeBackgroundColor:sender
{
    [self changeBackgroundColorTo:[sender color] andDisplay:YES];
    return self;
}

- (void)changeSourceColorTo:(NXColor)color andDisplay:(BOOL)flag
{
    if (!NXEqualColor(sourceColor, color)) {
	sourceColor = color;
	[source recache];
	[result recache];
	if (flag) [self display];
    }
}

- (void)changeDestColorTo:(NXColor)color andDisplay:(BOOL)flag
{
    if (!NXEqualColor(destColor, color)) {
	destColor = color;
	[destination recache];
	[result recache];
	if (flag) [self display];
    }
}

- (void)changeBackgroundColorTo:(NXColor)color andDisplay:(BOOL)flag
{
    if (!NXEqualColor(backgroundColor, color)) {
	backgroundColor = color;
	if (flag) [self display];
    }
}

// The operator method returns the operator currently in use.

- (int)operator {return operator;}


// setOperator sets the operator to be used in the compositing operations
// and updates the view to reflect the change. Note that setOperator needs
// to be connected to a row of buttons.

- setOperator:sender
{	
    switch ([sender selectedRow]) {
	case 0: operator = NX_COPY;		break;
	case 1: operator = NX_CLEAR; 		break;
	case 2: operator = NX_SOVER; 		break;
	case 3: operator = NX_DOVER;		break;
	case 4: operator = NX_SIN; 		break;
	case 5: operator = NX_DIN; 		break;
	case 6: operator = NX_SOUT;		break;
	case 7: operator = NX_DOUT;		break;
	case 8: operator = NX_SATOP;		break; 
	case 9: operator = NX_DATOP;		break;
	case 10: operator = NX_XOR; 		break;
	case 11: operator = NX_PLUSD;		break;
	case 12: operator = NX_PLUSL;		break;
	default: break;
    }
    [result recache];
    [self speedyDraw];

    return self;
}

		
// drawSelf:: simply redisplays the contents of the view. The source and
// destination rectangles are updated from the bitmaps while the result
// rectangle is created by compositing the two bitmaps.

- drawSelf:(NXRect *)r :(int) count
{
    // Erase the whole view
    NXSetColor (backgroundColor);
    NXRectFill (&bounds);

    // Color for the frame of the three sections...
    NXSetColor (NXChangeAlphaComponent (NX_COLORBLACK, 1.0));

    // Draw the source bitmap and then frame it with black
    [source composite:NX_SOVER toPoint:&sRect.origin];
    NXFrameRect(&sRect);

    // Draw the destination bitmap and frame it with black 
    [destination composite:NX_SOVER toPoint:&dRect.origin];
    NXFrameRect(&dRect);

    // And now for the result image. Frame it with black as well
    [result composite:NX_SOVER toPoint:&rRect.origin];
    NXFrameRect(&rRect);

    return self;
}

// speedyDraw provides some efficiency in redisplaying the view by assuming
// that the source and the destination rectangles are already in place.

- speedyDraw
{
    [self lockFocus];
    NXSetColor (backgroundColor);
    NXRectFill (&rRect);
    [result composite:NX_SOVER toPoint:&rRect.origin];
    NXSetColor (NXChangeAlphaComponent (NX_COLORBLACK, 1.0));
    NXFrameRect(&rRect);
    [[self window] flushWindow];
    [self unlockFocus];

    return self;
}

// free method to free all the images along with the view.

- free
{
    [source free];
    [destination free];
    [result free];
    [customImage free];
    return [super free];
}


// Code to support dragging...
// This is mostly complicated by the fact that the code wants to demonstrate
// how to dynamically give feedback to the user as the colors are
// dragged (but not dropped). We don't dynamically give feedback when images
// are dragged (as it might take a long time).

// includesType() returns YES if type is included within types.

static BOOL includesType (const NXAtom *types, NXAtom type)
{
    if (types) while (*types) if (*types++ == type) return YES;
    return NO;
}

- (NXDragOperation)draggingEntered:(id <NXDraggingInfo>)sender
{
    return [self draggingUpdated:sender];
}

- (NXDragOperation)draggingUpdated:(id <NXDraggingInfo>)sender
{
    if ([sender draggingSourceOperationMask] & NX_DragOperationGeneric) {
	Pasteboard *pboard = [sender draggingPasteboard];
        if (includesType([pboard types], NXColorPboardType)) {	// Color
	    NXColor sourceColorSave = sourceColor;
	    NXColor destColorSave = destColor;
	    NXColor backgroundColorSave = backgroundColor;
	    [self doColorDrag:sender];
	    [self changeSourceColorTo:sourceColorSave andDisplay:NO];
	    [self changeDestColorTo:destColorSave andDisplay:NO];
	    [self changeBackgroundColorTo:backgroundColorSave andDisplay:NO];
	    return NX_DragOperationGeneric;
	} else if ([NXImage canInitFromPasteboard:pboard]) {	// Image?
	    return NX_DragOperationGeneric;
	}
    }
    return NX_DragOperationNone;	    
}

- draggingExited:sender
{
    if (includesType([[sender draggingPasteboard] types], NXColorPboardType)) {	// We need to fix the view up
	[self display];
    }
    return self;
}

- (BOOL)performDragOperation:(id <NXDraggingInfo>)sender
{
    return ([self draggingUpdated:sender] == NX_DragOperationNone) ? NO : YES;
}

- concludeDragOperation:(id <NXDraggingInfo>)sender
{
    Pasteboard *pboard = [sender draggingPasteboard];

    if (includesType([pboard types], NXColorPboardType)) {
	[self doColorDrag:sender];
	[sourceColorWell setColor:sourceColor];
	[destColorWell setColor:destColor];
	[backColorWell setColor:backgroundColor];
    } else {
        (void)[self changeCustomImageTo:[[NXImage allocFromZone:[self zone]] initFromPasteboard:pboard]];
    }
    return self;
}

- (void)doColorDrag:(id <NXDraggingInfo>)sender
{
    NXPoint p = [sender draggingLocation];
    NXColor c = NXReadColorFromPasteboard([sender draggingPasteboard]);

    [self convertPoint:&p fromView:nil];

    switch ((int)(3 * p.x / NX_WIDTH(&bounds))) {
	case 0:
	    [self changeSourceColorTo:c andDisplay:YES];
	    break;
	case 1:
	    [self changeDestColorTo:c andDisplay:YES];
	    break;
	case 2:
	    [self changeBackgroundColorTo:c andDisplay:YES];
	    break;
	default:
	    break;	// Shouldn't really happen...
    }
}


@end
